Skip to content

Configurable redirect URI and free loopback port for browser login - #125

Merged
sionsmith merged 3 commits into
osodevops:mainfrom
balcsida:feat/configurable-redirect-uri
Oct 26, 2026
Merged

sionsmith merged 3 commits into
osodevops:mainfrom
balcsida:feat/configurable-redirect-uri

Conversation

@balcsida

Copy link
Copy Markdown
Contributor

The browser (authorization code + PKCE) login hardcoded the redirect URI http://localhost:8400/callback and listened only on 127.0.0.1:8400. That ruled out public client apps whose registered redirect URI has no path, such as Microsoft's own "Microsoft Graph Command Line Tools" app (14d82eec-204b-4c2f-b7e8-296a70dab67e, registered http://localhost), because the identity platform ignores the port of a localhost redirect URI but matches the path exactly. It also failed when port 8400 was busy, and missed the callback when the browser resolved localhost to ::1.

Behaviour changes:

  • --redirect-uri, TEAMS_CLI_REDIRECT_URI and the profile's redirect_uri select the redirect URI for auth login, auth consent-url and auth doctor, with the same precedence as the client and tenant IDs. A delegated login saves the flag value to the profile with the IDs; the env value is used but not saved.
  • The login listens on every loopback address the URI's host resolves to (both 127.0.0.1 and ::1 for localhost) and accepts the callback on any path. A URI must be http and point at localhost or a loopback IP literal; anything else is invalid input (exit 2) before a browser opens. A port held by another process is still an error, since that process could receive the authorization code.
  • A URI without a port gets a free port from the OS at each login, as MSAL does, and that port-bearing URI goes into both the authorize URL and the token exchange. An explicit port is sent exactly as given.
  • The default is now http://localhost/callback on a free port instead of http://localhost:8400/callback, so a busy port 8400 no longer blocks login. Registrations with http://localhost:8400/callback keep working because the port is ignored.

Usage:

# Microsoft's public Graph command line app, registered http://localhost (no path)
teams auth login --client-id 14d82eec-204b-4c2f-b7e8-296a70dab67e --tenant-id <tenant-id> --redirect-uri http://localhost

# Built-in OSO app: nothing to pass, a free port is picked
teams auth login

Review guide:

  • 27057f8 the configurable URI: resolve_redirect_uri and remember_registration in config.rs, loopback_addrs and bind_loopback in auth_code_pkce.rs.
  • d8960cd the free port: bind_loopback now returns the shared port, redirect_uri_with_port rewrites the URI, and the authorize URL is built after binding.

Testing: unit tests cover URI validation, the shared free port across listeners, the busy-port error and the URI rewrite; CLI tests cover the flag, env, profile and default for consent-url and doctor, and that a bad URI exits 2 for both the browser and device code flows. Which URIs the two apps accept was checked read-only against the authorize endpoint with prompt=none (a registered URI gets a login_required redirect, an unregistered one gets an inline error): the OSO app accepts http://localhost/callback on any port, the Microsoft app accepts http://localhost on any port, and neither accepts 127.0.0.1. No live login was run from this branch; the first real sign-in is the remaining check.

balcsida and others added 3 commits October 26, 2026 22:32
Each loopback listener served a single connection. With two listeners
(127.0.0.1 and ::1), a browser's speculative connection or an earlier
request could leave the login waiting forever, and a redirect carrying
the identity platform's error (a declined consent, say) did too; on
main both failed with "Failed to receive auth code from callback".

Serve every connection on every listener until a request to the
redirect URI's path carries this login's state, as RFC 8252 and MSAL do.
An error redirect with that state fails the login with error and
error_description (exit 3); other paths get 404, another state 400, and
the wait gives up after 10 minutes. Responses close the connection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sionsmith
sionsmith force-pushed the feat/configurable-redirect-uri branch from d8960cd to d183726 Compare October 26, 2026 22:36
@sionsmith

Copy link
Copy Markdown
Contributor

Thanks @balcsida, this is a good change. Here's what I verified and what I added.

Reproduced on main. I built throwaway binaries with the identity platform pointed at a local fake IdP and a script standing in for the browser:

  • Port 8400 busy: AUTH_FAILED: Failed to bind loopback server: Address already in use (exit 3).
  • Browser resolves localhost to ::1: the callback is refused and auth login hangs until killed.

Verified on this branch.

  • Both of the above sign in.
  • The authorize request and the token exchange carry the same http://localhost:<free-port>/callback, and the PKCE challenge matches the verifier.
  • The Microsoft Graph Command Line Tools app works end to end with --redirect-uri http://localhost. It sends http://localhost:<port>/ for both requests.
  • I also probed the live authorize endpoint read-only (prompt=none). The OSO app accepts http://localhost/callback on any port and rejects 127.0.0.1 and other paths. The Microsoft app accepts http://localhost, http://localhost:<port> and http://localhost:<port>/. So the new default is safe for existing users.

Found and fixed (d183726). Each listener served a single connection. With two listeners, that made two cases worse than on main:

  • A redirect carrying the identity platform's error (for example declined consent): main failed with a generic "Failed to receive auth code from callback"; this branch hung forever, because the other listener still held the sender.
  • A browser's speculative connection arriving first: main failed; this branch hung.

The receiver now follows RFC 8252 and MSAL practice:

  • It serves every connection on every listener until a request to the redirect URI's path carries this login's state.
  • That request's error and error_description fail the login immediately (exit 3), e.g. Sign-in failed: access_denied: AADSTS65004: User declined to consent.
  • Other paths get 404 and another state gets 400, and the login keeps waiting.
  • It gives up after 10 minutes with no callback.

Perplexity and the Entra redirect URI docs back this design. That meant replacing the PR's "accepts the callback on any path" with a path match, which works because Entra matches the path exactly anyway.

Tests and docs. I added unit tests over real sockets: stray connection, favicon, wrong state, missing code, error redirect, and timeout. The black-box harness now passes all scenarios (normal, ::1, preconnect, favicon, wrong state, error, Microsoft app). I updated CHANGELOG, README and troubleshooting, and rebased onto main (resolving the sha2 PKCE refactor from #124). fmt, clippy, the full test suite, mandoc and CI are all green.

@sionsmith
sionsmith merged commit ae83d60 into osodevops:main Oct 26, 2026
6 checks passed
@balcsida
balcsida deleted the feat/configurable-redirect-uri branch October 26, 2026 22:47
sionsmith added a commit to balcsida/ms-teams-cli that referenced this pull request Oct 26, 2026
…ommands

Bring the skills up to date with osodevops#119 and osodevops#125:
- automation: listen binds 127.0.0.1, takes the subscription's
  clientState through TEAMS_CLI_CLIENT_STATE, and answers /lifecycle;
  subscribe create gets --lifecycle-url. The lifetimes now match
  Graph's table (Teams messages and chats up to three days, presence one
  hour, 45 minutes minimum, lifecycle URL over an hour), replacing "at
  most one hour", and say what to do on each lifecycle event.
- setup: --redirect-uri for a bring-your-own app's browser login, what
  is saved to the profile, and what AADSTS50011, a "Sign-in failed"
  error and the 10-minute wait mean.

agent_skill_commands_parse runs every teams line in the skills' code
blocks through Cli::try_parse_from, so a renamed flag or a missing
argument fails the build instead of teaching agents a broken command.
AGENTS.md lists skills/ and adds it to the keep-in-sync rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sionsmith added a commit that referenced this pull request Oct 26, 2026
* feat(skills): add agent skills for setup, messaging, admin and automation

Four installable SKILL.md files under skills/, following the osodevops
skills convention (one focused skill per workflow surface, each with
agents/openai.yaml):

- teams-cli-setup: install, delegated sign-in, profiles, CI auth, the
  JSON envelope, exit codes and pagination
- teams-cli-messaging: find ids, read, send, reply, react, delete and
  restore, attachments and inline images, channel files, chats
- teams-cli-admin: teams, channels, members, tags, apps and tabs, with
  a confirmation procedure for destructive commands
- teams-cli-automation: presence, meetings, activity notifications,
  change-notification subscriptions, the local listener, and polling

Every command line in the skills was checked against the binary's
--help output. README gets an "Agent Skills" section with the
npx skills add install command; the docs index and CHANGELOG link to
the skills, and the attachments spec names the real skill file.

* feat(skills): cover listen hardening and --redirect-uri; test skill commands

Bring the skills up to date with #119 and #125:
- automation: listen binds 127.0.0.1, takes the subscription's
  clientState through TEAMS_CLI_CLIENT_STATE, and answers /lifecycle;
  subscribe create gets --lifecycle-url. The lifetimes now match
  Graph's table (Teams messages and chats up to three days, presence one
  hour, 45 minutes minimum, lifecycle URL over an hour), replacing "at
  most one hour", and say what to do on each lifecycle event.
- setup: --redirect-uri for a bring-your-own app's browser login, what
  is saved to the profile, and what AADSTS50011, a "Sign-in failed"
  error and the 10-minute wait mean.

agent_skill_commands_parse runs every teams line in the skills' code
blocks through Cli::try_parse_from, so a renamed flag or a missing
argument fails the build instead of teaching agents a broken command.
AGENTS.md lists skills/ and adds it to the keep-in-sync rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Sion Smith <sion.smith@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@sionsmith sionsmith mentioned this pull request Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants